本篇大綱:Domain & Range 輸入域與輸出域、Interpolate 插補值、continuous & discrete 連續性與離散性、scale 的五種三類、簡單比較
比例尺
是 D3 另一個很重要的功能!!因此,今天也會是很長的篇章啦(燦笑摸頭),大家請備好零食飲料,我們準備開始囉!
不知道大家有沒有看過「搞笑的網購商品」這系列呢?就是大家把自己網路上購買的商品 vs 實際收到貨的商品 拍照給大家看,其中有不少的照片長這樣
是不是超級搞笑,這是給小矮人的椅子嗎?賣家的圖片跟買家實際收到商品有一大段落差,是怎麼回事呢?這是因為賣方並沒有附上比例尺去告知這個椅子應該是多大,買家也就自行腦補正常椅子的比例才導致這樣的結果。我們的生活中充滿各種需要比例尺的時候,像是 google map、商品圖都會使用到比例尺。那這跟D3 圖表到底有什麼關係呢?別急,讓我們繼續看下去
之前在svg的篇章中我們有說過,svg 理論上是無限大的,它所設定的 width 跟 height 其實只是 viewport (視窗範圍)
。所以,即使我們的數據資料超過 svg 的 viewport,它一樣會存在跟繪製,只是我們看不到而已。就拿以下的範例來說明:
畫面上有個 svg ,它的視窗範圍:高100 X 寬500
當我們的線條長度為300,比 viewport 小:
當我們的線條長度為600,比 viewport 大:虛線的部分就是仍然存在但我們看不到的地方
由此可知,我們的資料需要在 svg 的 viewport 範圍內,我們才能完整看到所有的資訊,但是我們拿到的資料數據不可能這麼完美的符合svg的可視範圍呀,怎麼辦?這時就要派出 D3 的 scale 上場啦!其實 d3.scale 要做的事情很單純,就是比例換算,但比例換算也牽扯到非常多的細節,我們接著就來仔細的看一下吧!
D3 的 scale 方法是將資料(通常為陣列)轉換成視覺變量(visual variables),例如:位置、長度、顏色等等
,這樣一來我們才能使用視覺變量把資料視覺化。舉例而言,scale 可以把資料轉換成以下幾種視覺變量:
要講 scale 之前,我們要先來說說 domain & range (輸入域與輸出域)的概念。剛剛提到 scale 是在進行比例的換算,那既然是「換算」的話,那就要能產生換算前跟換算後的數值吧?這邊的 Domain 跟 Range 就是在處理這個概念
Domain 輸入域
是在進行比例尺換算前,資料的整個數值範圍Range 輸出域
則是進行比例尺換算後,得到換算之後的資料數值範圍一般來說,我們會將A範圍換算到B範圍的概念稱為映射。舉例來說,我們設定輸入域是 [0~100] 的範圍,輸出域是 [0~10] 的範圍,設定好這個範圍後當我們輸入數據50時,透過比例尺的換算,最後就會得到5的換算值
// 輸入與輸出比例換算範例
const convert = d3.scaleLinear()
.domain([0, 100])
.range([0, 10])
console.log(convert(50)); // 5,換算輸出比例完成
由於輸入域跟輸出域是比例尺必備的基本要素,因此每個比例尺的 API 旗下都會有 .domain( ) 跟 .range( )的方法。但看到這邊你有沒有覺得有點疑惑呢?為什麼設定輸入域跟輸出域的範圍後,我們隨便輸入的數值(但要在輸入域內)就能被換算成對應的輸出值呢?這是因為在比例尺的運作中,還使用了另一種重要方法:interpolate 插補值
插補是D3的一個蠻重要的應用,但若要詳細解說的話恐怕要另開一個長篇來講,因此我們這邊只會簡單講解它的原理與運作方式,有興趣了解更多的人可以自行上官方文件查看。
插補器說白了就是在兩個值之間平順的插入一些值,這個應用十分廣泛,例如:建立一個平順的動畫效果、設定一個漸層的顏色梯度,這些都是應用了插補的原理。而且 D3 的插補不僅可以應用在數值之間,還可以用在「日期、顏色、字串」等等各種資料型態間,是個非常神奇的功能。
對於插補器我們目前只要了解到這樣就行了,雖然 D3.scale的底層運作使用插補器,但我們並不需要完全了解這個運作也能順利地使用scale,因為困難的地方 D3 都幫我們做掉了XD。大致瞭解插補器在做什麼之後,我們還要知道另一個重要的小知識:連續值與離散值
。
連續性跟離散性是我們在使用scale 時也需要瞭解的一個觀念,它直接牽涉到 scale 的分類以及映射方式。先前我們提到 domain 輸入域與 range 輸出域的概念,連續性與離散性指的便是 domain 輸入域與 range 輸出域的映射方式。
continuous 連續性
:指的是資料之間具備關聯的特性,可以用某些運算方式找出彼此的關聯,這類資料通常為數字、日期等等discrete 離散性
:指的是則是資料之間並沒有任何關聯,無法用任何運算方式找出彼此的關聯,這類資料通常為字串將連續性或離散性搭配輸入域與輸出域的概念,就可以得出四種結果
上面四種輸入輸出資料的搭配組合,就成了 scale 的主要分類依據,接著來看D3總共提供哪些比例尺的方法吧!
D3 的官方文件將比例尺分成五種,分別是
Continuous Scale 連續性比例尺
Sequential Scale 序列比例尺
Diverging Scale 發散比例尺
Quantize Scale 量化比例尺
Ordinal Scale 次序/序位比例尺
但若按照輸入與輸出的資料來分類,這五類比例尺又可以被歸納為三大類:
「連續性資料輸入」與「連續性資料輸出」的比例尺
包含 Continuous Scale、Sequential Scale、Diverging Scale
「連續性資料輸入」與「離散性資料輸出」的比例尺
包含Quantize Scale
「離散性資料輸出」與「離散性資料輸出」的比例尺
包含 Ordinal Scale
以下就讓我們按照這三大類別來看看 D3 scale 的特色吧!
這一類的比例尺都是將一組連續性的資料,映射到另一個連續性的資料中,並依此對照去進行數值轉換。這一類比例尺又可以被細分成三小類:
Continuous Scale 連續性比例尺
連續性比例尺指的是資料可以被以某種運算方式找到關聯,像是月份的數值遞增、數字與數字間可以透過加減乘除找到規律等等,這些就叫做連續性比例尺;非連續性比例尺則是資料間無法透過運算找出關聯,例如:男女分類、喜歡的寵物(貓、狗、魚、兔)等等。
根據官方文件的敘述,連續性比例尺可以把連續的、定量的 domain (輸入域) 映射到連續的 range (輸出域)。而且如果輸出範圍也是數值,這個映射關係還可以被反轉 (使用 continuous.intert 方法),意思就是我們可以透過反推輸出的值去找到輸入的值。除了反轉之外,連續性比例尺還有這些不同的 API 可以進行相關設定,比較常用的設定我們晚點也會介紹
不過連續性比例尺只是大分類,不能直接使用,我們要使用它旗下的比例尺方法來進行操作,它旗下的方法包含:
雖然有這麼多比例尺,但我們比較常用到的只有 scaleLinear 跟 scaleTime 比例尺,我們接下來就仔細的講解一下吧!
★ d3.scaleLinear
線性比例尺是畫圖表時最常用到的比例尺,它最適合將資料轉換成位置或長度,通常會用在繪製折線圖。
線性比例尺的 domain 跟 range 都必須是連續性資料,而且由於是連續性資料,因此可以用陣列帶入最小值與最大值
即可,d3.scaleLinear 就會搭配 d3.Interpolate 去自動計算要輸出的數值
let linearScale = d3.scaleLinear()
.domain([0, 100])
.range([0, 50]);
linearScale(0); // return 0
linearScale(50); // returns 25
linearScale(100); // returns 50
除了轉換長度跟位置之外,線性比例尺也可以用來換算顏色的色度
const colorScale = d3.scaleLinear()
.domain([0, 10])
.range(['yellow', 'red']);
colorScale(0); // returns "rgb(255, 255, 0)"
colorScale(5); // returns "rgb(255, 128, 0)"
colorScale(10); // returns "rgb(255, 0, 0)"
★ d3.scaleTime
時間比例尺主要是用來換算日期、時間等等資料的方法,它的用法跟線性比例尺很類似,但不同的是時間比例尺的 domain 輸入域必需輸入日期陣列
timeScale = d3.scaleTime()
.domain([new Date(2021, 0, 1), new Date(2022, 0, 1)])
.range([0, 700]);
timeScale(new Date(2021, 0, 1)); // returns 0
timeScale(new Date(2021, 6, 1)); // returns 348.00...
timeScale(new Date(2022, 0, 1)); // returns 700
看完連續性比例尺的兩個方法後,接著我們來講講先前提到的細節設定吧
continuous.clamp( ) 截斷
我們瞭解 domain 跟 range的概念,也知道輸入domain範圍內的數字,就能夠被換算成相對應的range數值,但其實如果我們輸入超出domain範圍的數值也一樣能被換算。
let linearScale = d3.scaleLinear()
.domain([0, 10])
.range([0, 100]);
linearScale(20); // returns 200
linearScale(-10); // returns -100
如果我們不希望超出domain範圍的數值被換算,就可以使用 continuous.clamp( ) 這個方法,這個方法會將超過的數值直接換成domain 範圍的極端值
let linearScale = d3.scaleLinear()
.domain([0, 10])
.range([0, 100])
.clamp(true) // 斬斷鎖鏈~~~
linearScale(20); // returns 100
linearScale(-10); // returns 0
continuous.nice( )
這個 API 是用來延展 domain 的值,讓 domain 的起始值跟終止值變成比較漂亮的數值。有時候我們的domain範圍會直接從後端給的資料抓,但資料不一定是漂亮的數值,這樣反應到軸線上時可能就會讓軸線不那麼漂亮。
let data = [0.243, 0.584, 0.987, 0.153, 0.433];
let extent = d3.extent(data);
let linearScale = d3.scaleLinear()
.domain(extent)
.range([0, 100]);
畫出來的軸線是這樣,由於起始值跟終點值不在X軸線可以設定的範圍內,因此X軸的前後就沒有值 (有關軸線的建立下一篇會講解)
這時我們就可以使用 .nice( ) 這個方法讓起始值跟終點值變成漂亮的數值
let data = [0.243, 0.584, 0.987, 0.153, 0.433];
let extent = d3.extent(data);
let linearScale = d3.scaleLinear()
.domain(extent)
.range([0, 100])
.nice()
continuous.invert( ) 反推轉換
.invert( ) 這個方法把range的數值換算成domain的數值,通常用在軸線刻度的text顯示,這邊等到軸線的篇章會更仔細講解,目前只要知道它怎麼用就好
let linearScale = d3.scaleLinear()
.domain([0, 10])
.range([0, 100]);
linearScale.invert(50); // returns 5
linearScale.invert(100); // returns 10
Sequential Scale 序列比例尺
序列比例尺與連續性比例尺和發散比例尺很類似,一樣是將連續數值輸入域映射到連續數值的輸出域。但跟連續性比例尺不同的是: sequential scales 的輸出域是根據指定的內建插補器來進行設定,而且輸出域不可更動、插補方式也不可更動。舉例來說
let sequentialScale = d3.scaleSequential()
.domain([0, 100])
.interpolator(d3.interpolateRainbow);
這個例子中,我們設定了domain,但range的部分變成用 d3.interpolator( ) 取代,而且參數帶入d3內建好的 d3.interpolateRainbow 方法用來建立彩虹的色階,我們不可以自己任意改變成 range(...)
let sequentialScale = d3.scaleSequential()
.domain([0, 100])
.interpolator(d3.interpolateRainbow);
sequentialScale(0); // returns 'rgb(110, 64, 170)'
sequentialScale(50); // returns 'rgb(175, 240, 91)'
sequentialScale(100); // returns 'rgb(110, 64, 170)'
D3內建好的顏色插補器除了interpolatorRainbow 之外,還有許多其他不同的方法
有興趣的可以到 d3-scale-chromatic的官方文件來查看~這邊就不多做說明了
Diverging Scale 發散比例尺
發散比例尺是將一個「連續性、定量的輸入資料」轉換成「連續性、固定的插補器」,不過這個方法我自己到目前為止還沒有使用過~
const spectral = d3.scaleDiverging(d3.interpolateSpectral);
這一類的比例尺是將一組連續性的資料,映射到另一組離散性的資料中,並依此對照去進行轉換。這一類的比例尺被稱為「量化比例尺」
Quantize Scale 量化比例尺
量化比例尺包含以下幾個比例尺
這些API 中比較常用到的是 scaleQuantize 量化比例尺
,下面我們就來簡單介紹一下
★ d3.scaleQuantize
這個方法使接收一組連續性的數值,並映射到一組離散性的數值中,接著根據離散性數值的數量把連續性數值分成不同區段,再將輸入的數值映射到相對應的區段數值,舉例來說:
let quantizeScale = d3.scaleQuantize()
.domain([0, 100])
.range(['lightblue', 'orange', 'lightgreen', 'red']);
此處用 scaleQuantize 的方法,會把 [0-100] 的範圍根據range資料切段
— 0-24 ⇒ lightblue
— 25-49 ⇒ orange
— 50-74 ⇒ lightgreen
— 75-100 ⇒ red
我們輸入的數值就會根據這個區段去照到對應的值
quantizeScale(10); // returns 'lightblue'
quantizeScale(30); // returns 'orange'
quantizeScale(90); // returns 'red'
這類比例尺跟連續性比例尺都很常被使用,也很常拿來互相做比較。這一類的比例尺是將一組離散性的資料,映射到另一組離散性的資料中,並依此對照去進行轉換。由於輸入與輸出的均是離散性資料,而離散性資料之間是沒有相關聯的,因此使用這一類的比例尺時,一定要把要換算的資料一對一搭配好,否則未搭配到的資料就沒有辦法轉換。這一類的比例尺被稱為「次序/序位比例尺」
Ordinal Scale 次序/序位比例尺
次序/序位比例尺又包含以下三種比例尺API:
我們來一一說明一下這些API的使用方法
★ d3.scaleOrdinal 次序比例尺
次序比例尺會遍歷輸入的離散性資料 (必須是陣列),並一一映射到輸出的離散性資料(也必須是陣列)。由於數值中沒有關聯性,因此必須將所有要對應的資料都一一列出,如果輸入域的資料比輸出域多的話,輸出域的資料陣列會從頭重複運算
let myData = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
let ordinalScale = d3.scaleOrdinal()
.domain(myData)
.range(['black', 'red', 'green']);
ordinalScale('Jan'); // returns 'black';
ordinalScale('Feb'); // returns 'red';
ordinalScale('Mar'); // returns 'green';
ordinalScale('Apr'); // returns 'black'; range 從頭重複一次
如果輸入的數值不在domain輸入域資料內的話,會自動被加進domain中
ordinalScale('Monday'); // returns 'black';
★ d3.scaleBand 區段比例尺
這個方法最常用來繪製長條圖表,它不僅能用來建立長條狀幾何圖形,也會將圖形間的間距 (padding) 考慮進去。scaleBand 會將輸入域的資料傳換成輸出域的區段
domain輸入域的資料必須是陣列,陣列中的每筆資料代表一條長條圖;range 輸出域則定義圖表範圍的最小與最大值 (ex: 整張圖表的寬度)
let bandScale = d3.scaleBand()
.domain(['狗', '貓', '天竺鼠', '烏龜', '海豚']) // 共有五條長條圖
.range([0, 200]); // 整張圖表的範圍
scaleBand 這個方法會將 range 根據 domain 的數量去切分區段,然後根據這個區段的資料去計算長條圖的位置與寬度
bandScale('狗'); // returns 0
bandScale('貓'); // returns 40
bandScale('海豚'); // returns 160
scaleBand 也提供了一些細節設定的API,讓我們能依此去設定長條圖的寬度、間距等等
如果要設定長條圖的寬度,我們會使用 .bandwidth( )
這個方法
bandScale.bandwidth(); // returns 40
要設定長條圖間的間距則有以下兩個 API
.paddingInner( )
每條長條圖之間的距離.paddingOuter( )
第一條長條圖跟最後一條長條圖的距離★ d3.scalePoint 點比例尺
點比例尺跟區段比例尺很類似,但差別在於點比例尺是換算的是點的位置,區段比例尺則是換算區段的範圍
也因為兩個方法換算的方式不同,因此取出來的質也會有所差異
scaleBand
let bandScale = d3.scaleBand()
.domain(['狗', '貓', '天竺鼠', '烏龜', '海豚'])
.range([0, 200]);
bandScale('狗'); // returns 0
bandScale('貓'); // returns 40
bandScale('海豚'); // returns 160
scalePoint
let pointScale = d3.scalePoint()
.domain(['狗', '貓', '天竺鼠', '烏龜', '海豚'])
.range([0, 200]);
pointScale('狗'); // returns 0
pointScale('貓'); // returns 50
pointScale('海豚'); // returns 200
這樣有看出兩者的差異了嗎~瞭解 scalePoint 的用法後,我們也來看看它旗下的API,並且來進行一些相關細節設定吧
point.step( )
這個方法使用來求取兩個point之間的距離
let pointScale = d3.scalePoint()
.domain(['狗', '貓', '天竺鼠', '烏龜', '海豚'])
.range([0, 200]);
pointScale.step(); // returns 50 ,每個點之間的距離
point.padding( )
這個方法是用來設定第一個點跟最後一個點分別對外的距離
let pointScale = d3.scalePoint()
.domain(['狗', '貓', '天竺鼠', '烏龜', '海豚'])
.range([0, 200])
.padding(3)
看完以上的分類有沒有很崩潰?我只是想畫圖表啊!為什麼要把比例尺搞得這麼複雜?別緊張別擔心,畫圖表時比較常用的比例尺其實也只有 Continuous Scale
跟 Ordinal Scale
而已。我們最後再來簡單比較一下這兩種比例尺
連續性比例尺:連續性的比例尺,適用於連續性質的資料,舉例來說:時間、數值;折線圖
非連續性比例尺:非連續性的比例尺,適用於非連續性質的資料,舉例來說:性別分為男、女;長條圖
以上!終於講完了我的天,scale 真的有許多小細節要注意,而且它的運作也相對複雜不少,但如果掌握好它的使用方法,畫起圖表來真的事半功倍!
最後附上本章的程式碼與圖表 Github 、 Github Page,需要的人請自行取用~